<template>
	<!-- 本体部分 -->
	<div
		:id="id"
		:class="['vue-puzzle-vcode', { show_: show }]"
		@mousedown="onCloseMouseDown"
		@mouseup="onCloseMouseUp"
		@touchstart="onCloseMouseDown"
		@touchend="onCloseMouseUp"
	>
		<div class="vue-auth-box_" @mousedown.stop @touchstart.stop>
			<div class="auth-body_" :style="`height: ${canvasHeight}px`">
				<!-- 主图，有缺口 -->
				<canvas
					ref="canvas1"
					:width="canvasWidth"
					:height="canvasHeight"
					:style="`width:${canvasWidth}px;height:${canvasHeight}px`"
				/>
				<!-- 成功后显示的完整图 -->
				<canvas
					ref="canvas3"
					:class="['auth-canvas3_', { show: isSuccess }]"
					:width="canvasWidth"
					:height="canvasHeight"
					:style="`width:${canvasWidth}px;height:${canvasHeight}px`"
				/>
				<!-- 小图 -->
				<canvas
					class="auth-canvas2_"
					ref="canvas2"
					:width="puzzleBaseSize"
					:height="canvasHeight"
					:style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${
						styleWidth -
						sliderBaseSize -
						(puzzleBaseSize - sliderBaseSize) *
							((styleWidth - sliderBaseSize) / (canvasWidth - sliderBaseSize))
					}px)`"
				/>
				<div :class="['loading-box_', { hide_: !loading }]">
					<div class="loading-gif_">
						<span></span>
						<span></span>
						<span></span>
						<span></span>
						<span></span>
					</div>
				</div>
				<div
					:class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]"
				>
					{{ infoText }}
				</div>
				<div
					:class="['flash_', { show: isSuccess }]"
					:style="`transform: translateX(${
						isSuccess
							? `${canvasWidth + canvasHeight * 0.578}px`
							: `-${canvasHeight * 0.578}px`
					}) skew(-30deg, 0);`"
				></div>
				<img class="reset_" @click="reset" src="/assets/img/reset.png" />
			</div>
			<div class="auth-control_">
				<div class="range-box" :style="`height:${sliderBaseSize}px`">
					<div class="range-text">{{ sliderText }}</div>
					<div
						class="range-slider"
						ref="rangeSlider"
						:style="`width:${styleWidth}px`"
					>
						<div
							:class="['range-btn', { isDown: mouseDown }]"
							:style="`width:${sliderBaseSize}px`"
							@mousedown="onRangeMouseDown($event)"
							@touchstart="onRangeMouseDown($event)"
						>
							<div></div>
							<div></div>
							<div></div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</template>
<script setup lang="ts">
import { Ref, render } from 'vue';
import { VuePuzzleCodeProps } from '../login-captcha';
const props = defineProps(VuePuzzleCodeProps);

const emit = defineEmits<{
	(name: 'close'): void;
	(name: 'success', x: number): void;
	(name: 'fail', x: number): void;
}>();

/**
 * 鼠标点下去时父级的width
 */
let startWidth: Ref<number> = ref(50);
/**
 * 鼠标当前的偏移X
 */
let newX: Ref<number> = ref(0);
/**
 * 鼠标按下时的X
 */
let startX: Ref<number> = ref(0);
/**
 * 定时器
 */
let timer: any = null;
/**
 * 为了解决Mac上的click BUG
 */
let closeDown = false;
/**
 * 是否可以拉动滑动条
 */
let isCanSlide = false;
/**
 * 拼图的起始X
 */
let pinX: Ref<number> = ref(0);
/**
 * 拼图的起始Y
 */
let pinY: Ref<number> = ref(0);
/**
 * 用于自定义图片时不会随机到重复的图片
 */
let imgIndex: Ref<number> = ref(-1);
/**
 * 鼠标是否在按钮上按下
 */
const mouseDown: Ref<boolean> = ref(false);
/**
 * 是否正在加在中，主要是等图片onload
 */
const loading: Ref<boolean> = ref(false);
/**
 * 提示等信息
 */
const infoText: Ref<string> = ref('');
/**
 * 是否验证失败
 */
const infoBoxFail: Ref<boolean> = ref(false);
/**
 * 提示信息是否出现
 */
const infoBoxShow: Ref<boolean> = ref(false);
/**
 * 验证成功
 */
const isSuccess: Ref<boolean> = ref(false);
const vuePuzzleCode: Ref<any> = ref(null);
const rangeSlider: Ref<any> = ref(null);
const canvas1: Ref<any> = ref(null);
const canvas2: Ref<any> = ref(null);
const canvas3: Ref<any> = ref(null);

onMounted(() => {
	const instance = getCurrentInstance();
	if (instance) {
		render(instance.vnode, document.body);
	}
	document.addEventListener('mousemove', onRangeMouseMove, false);
	document.addEventListener('mouseup', onRangeMouseUp, false);
	document.addEventListener('touchmove', onRangeMouseMove, false);
	document.addEventListener('touchend', onRangeMouseUp, false);
	if (props.show) {
		document.body.classList.add('vue-puzzle-overflow');
	}
	reset();
});

onUnmounted(() => {
	clearTimeout(timer);
	const instance = document.getElementById(props.id);
	if (instance) {
		document.body.removeChild(instance);
	}
	document.removeEventListener('mousemove', onRangeMouseMove, false);
	document.removeEventListener('mouseup', onRangeMouseUp, false);
	document.removeEventListener('touchmove', onRangeMouseMove, false);
	document.removeEventListener('touchend', onRangeMouseUp, false);
});

watch(
	() => props.show,
	(newVal, oldVal) => {
		if (newVal) {
			document.body.classList.add('vue-puzzle-overflow');
			reset();
		} else {
			document.body.classList.remove('vue-puzzle-overflow');
		}
	}
);

const styleWidth = computed(() => {
	const w = startWidth.value + newX.value - startX.value;
	return w < sliderBaseSize.value
		? sliderBaseSize.value
		: w > props.canvasWidth
		? props.canvasWidth
		: w;
});

const puzzleBaseSize = computed(() => {
	return Math.round(Math.max(Math.min(props.puzzleScale, 2), 0.2) * 52.5 + 6);
});

const sliderBaseSize = computed(() => {
	return Math.max(
		Math.min(Math.round(props.sliderSize), Math.round(props.canvasWidth * 0.5)),
		10
	);
});
const onClose = () => {
	if (!mouseDown.value) {
		clearTimeout(timer);
		emit('close');
	}
};
const onCloseMouseDown = () => {
	closeDown = true;
};
const onCloseMouseUp = () => {
	if (closeDown) {
		onClose();
	}
	closeDown = false;
};
const onRangeMouseDown = (e: any) => {
	if (isCanSlide) {
		mouseDown.value = true;
		startWidth.value = rangeSlider.value.clientWidth;
		newX.value = e.clientX || e.changedTouches[0].clientX;
		startX.value = e.clientX || e.changedTouches[0].clientX;
	}
};
const onRangeMouseMove = (e: any) => {
	if (mouseDown.value) {
		e.preventDefault();
		newX.value = e.clientX || e.changedTouches[0].clientX;
	}
};
const onRangeMouseUp = () => {
	if (mouseDown.value) {
		mouseDown.value = false;
		submit();
	}
};

const init = (withCanvas = false) => {
	loading.value = true;
	isCanSlide = false;
	const c = canvas1.value;
	const c2 = canvas2.value;
	const c3 = canvas3.value;
	const ctx = c.getContext('2d');
	const ctx2 = c2.getContext('2d');
	const ctx3 = c3.getContext('2d');
	const img = document.createElement('img');
	ctx.clearRect(0, 0, props.canvasWidth, props.canvasHeight);
	ctx2.clearRect(0, 0, props.canvasWidth, props.canvasHeight);

	// 取一个随机坐标，作为拼图块的位置
	pinX.value = getRandom(
		puzzleBaseSize.value,
		props.canvasWidth - puzzleBaseSize.value - 20
	); // 留20的边距
	pinY.value = getRandom(20, props.canvasHeight - puzzleBaseSize.value - 20); // 主图高度 - 拼图块自身高度 - 20边距
	img.crossOrigin = 'anonymous'; // 匿名，想要获取跨域的图片
	img.onload = () => {
		const [x, y, w, h] = makeImgSize(img);
		ctx.save();
		// 先画小图
		paintBrick(ctx);
		ctx.closePath();
		if (
			!(
				navigator.userAgent.indexOf('Firefox') >= 0 &&
				navigator.userAgent.indexOf('Windows') >= 0
			)
		) {
			// 非火狐，在此画外阴影
			ctx.shadowOffsetX = 0;
			ctx.shadowOffsetY = 0;
			ctx.shadowColor = '#000';
			ctx.shadowBlur = 3;
			ctx.fill();
		}

		ctx.clip(); // 按照外阴影区域切割

		ctx.save();
		// 小图外阴影
		ctx.shadowOffsetX = 0;
		ctx.shadowOffsetY = 0;
		ctx.shadowColor = '#000';
		ctx.shadowBlur = 2;
		ctx.fill();
		ctx.restore();
		ctx.drawImage(img, x, y, w, h);
		ctx3.drawImage(img, x, y, w, h);

		// 设置小图的内阴影
		ctx.globalCompositeOperation = 'source-atop';

		paintBrick(ctx);

		ctx.arc(
			pinX.value + Math.ceil(puzzleBaseSize.value / 2),
			pinY.value + Math.ceil(puzzleBaseSize.value / 2),
			puzzleBaseSize.value * 1.2,
			0,
			Math.PI * 2,
			true
		);
		ctx.closePath();
		ctx.shadowColor = 'rgba(255, 255, 255, .8)';
		ctx.shadowOffsetX = -1;
		ctx.shadowOffsetY = -1;
		ctx.shadowBlur = Math.min(Math.ceil(8 * props.puzzleScale), 12);
		ctx.fillStyle = '#ffffaa';
		ctx.fill();

		// 将小图赋值给ctx2
		const imgData = ctx.getImageData(
			pinX.value - 3, // 为了阴影 是从-3px开始截取，判定的时候要+3px
			pinY.value - 20,
			pinX.value + puzzleBaseSize.value + 5,
			pinY.value + puzzleBaseSize.value + 5
		);
		ctx2.putImageData(imgData, 0, pinY.value - 20);

		// 清理
		ctx.restore();
		ctx.clearRect(0, 0, props.canvasWidth, props.canvasHeight);

		// 画缺口
		ctx.save();
		paintBrick(ctx);
		ctx.globalAlpha = 0.8;
		ctx.fillStyle = '#ffffff';
		ctx.fill();
		ctx.restore();

		// 画缺口的内阴影
		ctx.save();
		ctx.globalCompositeOperation = 'source-atop';
		paintBrick(ctx);
		ctx.arc(
			pinX.value + Math.ceil(puzzleBaseSize.value / 2),
			pinY.value + Math.ceil(puzzleBaseSize.value / 2),
			puzzleBaseSize.value * 1.2,
			0,
			Math.PI * 2,
			true
		);
		ctx.shadowColor = '#000';
		ctx.shadowOffsetX = 2;
		ctx.shadowOffsetY = 2;
		ctx.shadowBlur = 16;
		ctx.fill();
		ctx.restore();

		// 画整体背景图
		ctx.save();
		ctx.globalCompositeOperation = 'destination-over';
		ctx.drawImage(img, x, y, w, h);
		ctx.restore();

		loading.value = false;
		isCanSlide = true;
	};

	img.onerror = () => {
		init(true); // 如果图片加载错误就重新来，并强制用canvas随机作图
	};

	if (!withCanvas && props.imgs && props.imgs.length) {
		let randomNum = getRandom(0, props.imgs.length - 1);
		if (randomNum === imgIndex.value) {
			if (randomNum === props.imgs.length - 1) {
				randomNum = 0;
			} else {
				randomNum++;
			}
		}
		imgIndex.value = randomNum;
		img.src = props.imgs[randomNum];
	} else {
		img.src = makeImgWithCanvas();
	}
};
const getRandom = (min: number, max: number) => {
	return Math.ceil(Math.random() * (max - min) + min);
};
const makeImgSize = (img: HTMLImageElement) => {
	const imgScale = img.width / img.height;
	const canvasScale = props.canvasWidth / props.canvasHeight;
	let x = 0,
		y = 0,
		w = 0,
		h = 0;
	if (imgScale > canvasScale) {
		h = props.canvasHeight;
		w = imgScale * h;
		y = 0;
		x = (props.canvasWidth - w) / 2;
	} else {
		w = props.canvasWidth;
		h = w / imgScale;
		x = 0;
		y = (props.canvasHeight - h) / 2;
	}
	return [x, y, w, h];
};
const paintBrick = (ctx: any) => {
	const moveL = Math.ceil(15 * props.puzzleScale); // 直线移动的基础距离
	ctx.beginPath();
	ctx.moveTo(pinX.value, pinY.value);
	ctx.lineTo(pinX.value + moveL, pinY.value);
	ctx.arcTo(
		pinX.value + moveL,
		pinY.value - moveL / 2,
		pinX.value + moveL + moveL / 2,
		pinY.value - moveL / 2,
		moveL / 2
	);
	ctx.arcTo(
		pinX.value + moveL + moveL,
		pinY.value - moveL / 2,
		pinX.value + moveL + moveL,
		pinY,
		moveL / 2
	);
	ctx.lineTo(pinX.value + moveL + moveL + moveL, pinY.value);
	ctx.lineTo(pinX.value + moveL + moveL + moveL, pinY.value + moveL);
	ctx.arcTo(
		pinX.value + moveL + moveL + moveL + moveL / 2,
		pinY.value + moveL,
		pinX.value + moveL + moveL + moveL + moveL / 2,
		pinY.value + moveL + moveL / 2,
		moveL / 2
	);
	ctx.arcTo(
		pinX.value + moveL + moveL + moveL + moveL / 2,
		pinY.value + moveL + moveL,
		pinX.value + moveL + moveL + moveL,
		pinY.value + moveL + moveL,
		moveL / 2
	);
	ctx.lineTo(
		pinX.value + moveL + moveL + moveL,
		pinY.value + moveL + moveL + moveL
	);
	ctx.lineTo(pinX, pinY.value + moveL + moveL + moveL);
	ctx.lineTo(pinX, pinY.value + moveL + moveL);

	ctx.arcTo(
		pinX.value + moveL / 2,
		pinY.value + moveL + moveL,
		pinX.value + moveL / 2,
		pinY.value + moveL + moveL / 2,
		moveL / 2
	);
	ctx.arcTo(
		pinX.value + moveL / 2,
		pinY.value + moveL,
		pinX,
		pinY.value + moveL,
		moveL / 2
	);
	ctx.lineTo(pinX, pinY);
};
const makeImgWithCanvas = () => {
	const canvas = document.createElement('canvas');
	const ctx = canvas.getContext('2d');
	canvas.width = props.canvasWidth;
	canvas.height = props.canvasHeight;
	if (ctx) {
		ctx.fillStyle = `rgb(${getRandom(100, 255)},${getRandom(
			100,
			255
		)},${getRandom(100, 255)})`;
		ctx.fillRect(0, 0, props.canvasWidth, props.canvasHeight);
		// 随机画10个图形
		for (let i = 0; i < 12; i++) {
			ctx.fillStyle = `rgb(${getRandom(100, 255)},${getRandom(
				100,
				255
			)},${getRandom(100, 255)})`;
			ctx.strokeStyle = `rgb(${getRandom(100, 255)},${getRandom(
				100,
				255
			)},${getRandom(100, 255)})`;
			if (getRandom(0, 2) > 1) {
				// 矩形
				ctx.save();
				ctx.rotate((getRandom(-90, 90) * Math.PI) / 180);
				ctx.fillRect(
					getRandom(-20, canvas.width - 20),
					getRandom(-20, canvas.height - 20),
					getRandom(10, canvas.width / 2 + 10),
					getRandom(10, canvas.height / 2 + 10)
				);
				ctx.restore();
			} else {
				// 圆
				ctx.beginPath();
				const ran = getRandom(-Math.PI, Math.PI);
				ctx.arc(
					getRandom(0, canvas.width),
					getRandom(0, canvas.height),
					getRandom(10, canvas.height / 2 + 10),
					ran,
					ran + Math.PI * 1.5
				);
				ctx.closePath();
				ctx.fill();
			}
		}
	}
	return canvas.toDataURL('image/png');
};
const submit = () => {
	// 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * （用户真滑动的距离/canvas总宽度）
	// 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
	const x = Math.abs(
		pinX.value -
			(styleWidth.value - sliderBaseSize.value) +
			(puzzleBaseSize.value - sliderBaseSize.value) *
				((styleWidth.value - sliderBaseSize.value) /
					(props.canvasWidth - sliderBaseSize.value)) -
			3
	);
	if (x < props.range) {
		// 成功
		infoText.value = props.successText;
		infoBoxFail.value = false;
		infoBoxShow.value = true;
		isCanSlide = false;
		isSuccess.value = true;
		// 成功后准备关闭
		clearTimeout(timer);
		timer = setTimeout(() => {
			emit('success', x);
		}, 800);
	} else {
		// 失败
		infoText.value = props.failText;
		infoBoxFail.value = true;
		infoBoxShow.value = true;
		isCanSlide = false;
		// 失败的回调
		emit('fail', x);
		// 800ms后重置
		clearTimeout(timer);
		timer = setTimeout(() => {
			reset();
		}, 800);
	}
};
const reset = () => {
	infoBoxFail.value = false;
	infoBoxShow.value = false;
	isCanSlide = true;
	isSuccess.value = false;
	startWidth.value = sliderBaseSize.value; // 鼠标点下去时父级的width
	startX.value = 0; // 鼠标按下时的X
	newX.value = 0; // 鼠标当前的偏移X
	init();
};
</script>
